package jxerializeml.xml.builder;

import java.io.StringWriter;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import jxerializeml.errors.output.XMLBuilderError;
import jxerializeml.xml.FieldBean;
import jxerializeml.xml.NodeBean;
import jxerializeml.xml.transformers.OutTransformer;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * <p>
 *   Builds XML document using data you've passed via simple beans.
 *   Purpose of this class is to separate creation of the XML document
 *   from actual algorithm which somehow generates the data.
 * </p>
 * <p>
 *   Structure of the generate document is non-changeable, but each tag
 *   will be actually generated by the OutTransformer you've chosen.
 * </p>
 * 
 * @author Tomáš Zíma
 */
public class XMLBuilder {
	/** Generated XML document. */
	private Document xmlDocument = null;
	
	/**
	 * Last generated element on the given level of hierarchy. Used for purposes
	 * of methods {@link #enterNode()} and {@link #leaveNode()}.
	 */
	private Element xmlLastElement = null;
	
	/**
	 * Transforms FieldBeans into NodeBeans, which are later inserted into the
	 * document.
	 */
	private OutTransformer xmlOutTransformer = null;
	
	/**
	 * Creates root element, which will represent the serialized object.
	 * 
	 * @param className
	 *     Class name of the serialized object.
	 * 
	 * @param outTransformer
	 *     Instance of the transformer, which will generate nodes from
	 *     beans.
	 *     
	 * @throws XMLBuilderError
	 *     This exception will be thrown when creation of the XML builder failed due to
	 *     invalid configuration of the DocumentBuilder (provided by Java Core API).
	 */
	public XMLBuilder(String className, OutTransformer outTransformer) throws XMLBuilderError {
		try {
    		xmlDocument         = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
    		xmlLastElement      = createElement(outTransformer.transformRoot(className));
    		xmlOutTransformer   = outTransformer;
    		
    		xmlDocument.appendChild(xmlLastElement);
		} catch (ParserConfigurationException parserConfigurationException) {
			throw new XMLBuilderError(
				"XMLBuilder couldn't be created due to invalid configuration of DocumentBuilder.",
				parserConfigurationException
			);
		}
	}
	
	/**
	 * Adds a new element defining the field below the last entered node.
	 * 
	 * @param fieldBean
	 *   Represents all informations about the the field. Nothing else
	 *   will be used.
	 */
	public void addField(FieldBean fieldBean) {
		NodeBean    nodeBean        = xmlOutTransformer.transformField(fieldBean);
		Element     fieldElement    = createElement(nodeBean);

		if (fieldBean.hasComment()) {
			xmlLastElement.appendChild(
				xmlDocument.createComment(
					fieldBean.getComment()
				)
			);
		}
		
		xmlLastElement.appendChild(fieldElement);
	}
	
	/**
	 * Adds a new element defining one item in array (one index) below the last
	 * entered node.
	 * 
	 * @param fieldBean
	 *   Represents all informations about the item. Nothing else will be
	 *   used. In this case, index of the item is transfered as a "name".
	 */
	public void addArrayItem(FieldBean fieldBean) {
		NodeBean    nodeBean        = xmlOutTransformer.transformArrayItem(fieldBean);
		Element     itemElement     = createElement(nodeBean);

		xmlLastElement.appendChild(itemElement);
	}
	
	/**
	 * Enters last generated node (field or array's item). After that, each
	 * other call of the methods {@link #addField(FieldBean)} or
	 * {@link #addArrayItem(FieldBean)} will add new element under this last
	 * node.
	 * 
	 * @see #leaveNode()
	 */
	public void enterNode() {
		xmlLastElement = (Element) xmlLastElement.getLastChild();
	}
	
	/**
	 * Leaves current node and goes by one level higher in the hierarchy. After
	 * that, each other call of the method {@link #addField(FieldBean)} or
	 * {@link #addArrayItem(FieldBean)} will result in adding of the new element
	 * under this "higher" node.
	 * 
	 * @see #enterNode()
	 */
	public void leaveNode() {
		xmlLastElement = (Element) xmlLastElement.getParentNode();
	}
	
	/**
	 * Returns generated XML document as a text string. Nodes will be properly
	 * indented by 4 spaces.
	 * 
	 * @return
	 *     Generated XML document as a text string.
	 *   
	 * @throws XMLBuilderError
	 *     This exception will be thrown when creation of the Transformer, which
	 *     is provided by Java Core API, failed.
	 */
	public String getResult() throws XMLBuilderError {
		try {
    		Transformer transformer = TransformerFactory.newInstance().newTransformer();
    		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
    		transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
    		transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
    		
    		StringWriter writer = new StringWriter();
    		transformer.transform(new DOMSource(xmlDocument), new StreamResult(writer));
    		
    		return writer.getBuffer().toString();
		} catch (TransformerException transformerException) {
			throw new XMLBuilderError(
				"Transformer (provided by Java Core API) couldn't be created.",
				transformerException
			);
		}
	}
	
	/**
	 * Creates new element from the NodeBean (which should be generated by
	 * OutTransformer). Basically, it only copies tag name and all attributes
	 * from that.
	 * 
	 * @param nodeBean
	 *   NodeBean describing the node (tag name and all attributes).
	 * 
	 * @return
	 *   Instance of the Element, which can be directly inserted into the
	 *   XML document.
	 */
	protected Element createElement(NodeBean nodeBean) {
		Element     element = xmlDocument.createElement(nodeBean.getTagName());
		
		Object[]    keys    = nodeBean.getAttributes().keySet().toArray();
		Object[]    values  = nodeBean.getAttributes().values().toArray();
		
		for (int i = 0; i < nodeBean.getAttributes().size(); i++) {
			element.setAttribute(
				keys[i].toString(),
				values[i].toString()
			);
		}
		
		return element;
	}
}
